# koa中间件开发和使用
30 秒速记
Koa中间件 = 洋葱模型:请求一层层进去,响应一层层出来Koa 2写法async (ctx, next) => {},await next()前是前置逻辑,后面是后置逻辑Koa 1用function* (next)+yield next,this就是上下文;Koa 2要用koa-convert包一下才能跑app.use的注册顺序就是执行顺序,错误处理、计时这类中间件放最前面- 易错:忘了
await/returnnext(),下游的异步结果来不及写进ctx.body
Koa 中间件说白了就是一串按注册顺序套起来的函数,每个函数通过 await next() 把控制权交给下一层,等下游跑完再回到自己。 所以一个中间件天然分成前后两半:前半段可以做鉴权、记开始时间,后半段可以算耗时、改响应头。Koa 1 时代没有 async,靠 generator 加 co 模拟同样的效果;Koa 2 换成了原生 async/await,老的 generator 中间件得用 koa-convert 转一下。我写中间件一般都套一层工厂函数,配置在外层闭包里处理好,返回真正的 async (ctx, next)。
理解「koa中间件开发和使用」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
Koa 的中间件机制核心是 koa-compose,它把 app.use 注册的数组组合成一个函数:第 i 个中间件拿到的 next 就是 () => dispatch(i + 1),返回的永远是 Promise。下面这段可以直接跑,看清洋葱模型的执行顺序:
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
await next()
console.log(4)
})
app.use(async (ctx) => {
console.log(5)
ctx.body = 'ok'
})
app.listen(3000)
// 访问一次后输出:1 3 5 4 2
版本上要分清三档:Koa 1 只认 generator 中间件,上下文是 this;Koa 2 用 async (ctx, next),传入 generator 会打废弃警告并自动转换;Koa 3 已经去掉了对 generator 中间件的支持,老插件必须换掉或者自己改写成 async。
面试官追问
追问 1A、B、C 在 next 前打印 1 2 3,之后打印 4 5 6,最终输出什么?
1 2 3 6 5 4:前置部分按注册顺序走,后置部分倒过来。这就是洋葱模型,进去是 A → B → C,出来是 C → B → A。
追问 2全局错误处理中间件为什么一定要第一个 use?
它要用 try { await next() } catch (err) {} 包住所有下游,只有排在最外层,后面任何一层抛的错才会冒泡到它这里。放在路由后面的话,路由里抛的错它根本接不到。
追问 3某个中间件里没调用 next(),会发生什么?
请求在这一层就短路了,后面的中间件和路由都不会执行,响应就是当前已经写进 ctx 的内容。鉴权失败直接 ctx.status = 401 然后不调 next,就是故意利用这一点。
追问 4await next() 和 return next() 有区别吗?
都能让下游的 Promise 串进当前中间件,效果一样。区别是 return next() 之后就没有后置逻辑了;要在下游结束后做事,比如统计耗时,就得用 await next()。
追问 5多个中间件之间想共享数据,放哪里?
放 ctx.state,这是 Koa 官方约定的命名空间,模板渲染也会读它。比如鉴权中间件写 ctx.state.user,后面的路由直接取,别往 ctx 上随手挂字段,容易和别的插件撞名。
# koa中间件开发和使用
注:原文地址在我的博客issue里https://github.com/ChenShenhai/blog/issues/15
- koa v1和v2中使用到的中间件的开发和使用
- generator 中间件开发在koa v1和v2中使用
- async await 中间件开发和只能在koa v2中使用
# generator中间件开发
30 秒速记
generator中间件是Koa 1的写法:function* (next) {},靠co驱动执行yield next相当于今天的await next(),把控制权交给下游- 上下文是
this,不是参数ctx,所以不能写成箭头函数 Koa 2里要用koa-convert转换,Koa 3已经不支持- 新项目不要再写,读老代码、升级老项目时才需要懂
generator 中间件是 async/await 普及之前的方案,用 yield 暂停函数、让 co 这个库帮你自动往下执行,效果和现在的 await 几乎一样。 当年 Node 还不支持 async,TJ 就用 generator 加 co 让异步代码写得像同步。所以 yield next 你可以直接翻译成 await next(),this 翻译成 ctx。现在学它主要是为了看懂老项目:比如一个 Koa 1 的服务要升级,最常见的活就是把一堆 function* 改成 async (ctx, next)。
理解「generator中间件开发」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1generator 函数自己并不会自动跑完,Koa 1 是怎么让它执行下去的?
靠 co。它拿到 generator 的迭代器后不停调 next(),每遇到 yield 一个 Promise 或另一个 generator,就等它完成再把结果塞回去继续执行。async/await 本质上就是语言层面内置了这套自动执行器。
追问 2能不能把 generator 中间件写成箭头函数?
不能,箭头函数没有 function* 形式,也没有自己的 this。Koa 1 的上下文就是通过 this 传进来的,写成普通函数也拿不到 ctx。
追问 3接手一个 Koa 1 项目要升 Koa 2,中间件怎么迁?
先整体包 koa-convert 让服务跑起来,再逐个改写:function* (next) 改 async (ctx, next),this 改 ctx,yield next 改 await next(),yield somePromise 改 await。改完一个删一个 convert,别长期混用。
追问 4yield next 和 yield* next 有什么不一样?
yield* next 直接委托给下游 generator,少一层 co 包装,性能略好,Koa 1 源码里自己也这么用。日常写 yield next 就行,co 会帮你处理。
# generator中间件开发
30 秒速记
- 写成工厂函数:外层
function () {}接配置,返回function* (next) {} - 在
generator里用this访问上下文:this.method、this.url、this.header yield next交出控制权,后面的代码在下游执行完之后才跑if (next)判断是为了中间件被单独调用时不报错,挂在Koa 1上next一定有值- 忘写
yield next= 请求在这层短路
generator 中间件就是一个返回 generator 函数的工厂函数,里面用 this 拿请求信息,用 yield next 把请求交给下一层。 比如日志中间件,yield next 前面打印请求方法和地址,如果想顺便打印耗时,就在 yield next 后面再记一次时间。外面包一层工厂函数是社区习惯,方便传配置,比如 logger({ level: 'info' }),配置只在启动时处理一次,不用每个请求都算。
generator中间件返回的应该是function * () 函数
/* ./middleware/logger-generator.js */
function log( ctx ) {
console.log( ctx.method, ctx.header.host + ctx.url )
}
module.exports = function () {
return function * ( next ) {
